package com.devunion.salon.core.query;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.StringUtils;

import java.util.*;

/**
 * @author spetrakovsky
 */
public class QueryBuilder {

    private String alias;

    private String orderProperty;

    private StringBuilder query = new StringBuilder();

    private List parameters = new ArrayList();

    private boolean first = true;

    private boolean sortAscending = true;

    public static final Object NULL_VALUE = new Object();
    public static final Long NULL_LONG = new Long(-1);
    public static final String NULL_STRING = new String();
    public static final Set NULL_SET = new HashSet();
    public static final Date NULL_DATE = new Date();

    private static final String AND = " and ";
    private static final String WHERE = " where ";
    private static final String ORDER_BY = " order by ";
    private static final String ASC = " asc ";
    private static final String DESC = " desc ";
    private static final String EQUALS = " = ";
    private static final String EQ_OR_LESS = " <= ";
    private static final String MORE_OR_EQ = " >= ";
    private static final String BETWEEN = " between  ? and ? ";
    private static final String OR = " or ";
    private static final String IS_NULL = " is null ";
    private static final String OPEN_BRACKET = " ( ";
    private static final String CLOSE_BRACKET = " ) ";
    private static final char DOT = '.';
    private static final String LIKE_OPEN = " upper(";
    private static final String LIKE_CLOSE = ") like upper(?)";

    public QueryBuilder(String alias) {
        this(alias, null, true);
    }

    public QueryBuilder(String alias, String orderProperty) {
        this(alias, orderProperty, true);
    }

    public QueryBuilder(String alias, String orderProperty, boolean sortAscending) {
        this.alias = alias;
        this.orderProperty = orderProperty;
        this.sortAscending = sortAscending;
    }

    public Object[] getParameters() {
        return parameters.toArray();
    }

    public String getOrderProperty() {
        return orderProperty;
    }

    public void setOrderProperty(String orderProperty) {
        this.orderProperty = orderProperty;
    }

    public void appendCondition(String preposition, Object object, Object[][] properties) {
        if (object == null) {
            return;
        }
        if (properties == null) {
            return;
        }
        for (int i = 0; i < properties[0].length; i++) {
            Object objectProperty = properties[0][i];
            String entityPropery = (String) properties[1][i];
            Object value = getProperty(object, objectProperty);
            if (!skipValue(value)) {
                String expression = getExpression(entityPropery, alias, value);
                appendCondition(preposition, expression);
            }
        }
    }

    public void appendCondition(Object object, Object[][] properties) {
        appendCondition(AND, object, properties);
    }

    public void appendAndCondition(Object object, Object[][] properties) {
        appendCondition(AND, object, properties);
    }

    public void appendOrCondition(Object object, Object[][] properties) {
        appendCondition(OR, object, properties);
    }

    private void appendCondition(String preposition, String condition) {
        if (!StringUtils.isBlank(condition)) {
            if (!first) {
                query.append(preposition);
            } else {
                first = false;
            }
            query.append(condition);
        }
    }

    public void appendCustomCondition(String preposition, String condition) {
        appendCondition(preposition, condition);
    }

    public void appendOrCustomCondition(String condition) {
        appendCondition(OR, condition);
    }

    public void appendAndCustomCondition(String condition) {
        appendCondition(AND, condition);
    }

    public void appendCustomCondition(String condition) {
        appendCondition(AND, condition);
    }

    public void appendAnotherCondition(Object object, Object[][] properties) {
        if (object == null) {
            return;
        }
        boolean needBracket = false;
        if (!first) {
            query.append(OR).append(OPEN_BRACKET);
            needBracket = true;
            first = true;
        } else {
            first = false;
        }
        appendAndCondition(object, properties);
        if (needBracket) {
            query.append(CLOSE_BRACKET);
        }
    }

    public QueryBuilder junction(String preposition, QueryBuilder builder) {
        if (builder != null && !builder.first) {
            if (!first) {
                query.append(preposition).append(OPEN_BRACKET);
            }

            query.append(builder.query.toString());
            parameters.addAll(builder.parameters);

            if (!first) {
                query.append(CLOSE_BRACKET);
            } else {
                first = false;
            }
        }
        return this;
    }

    public QueryBuilder conjunction(QueryBuilder builder) {
        return junction(AND, builder);
    }

    public QueryBuilder disjunction(QueryBuilder builder) {
        return junction(OR, builder);
    }

    public String getQueryString() {
        return getQuery().toString();
    }

    public StringBuilder getQuery() {
        StringBuilder result = new StringBuilder();
        String where = query.toString();
        if (!StringUtils.isBlank(where)) {
            result.append(WHERE).append(where);
        }
        if (orderProperty != null) {
            result.append(ORDER_BY).append(alias).append(DOT).append(orderProperty)
                    .append(sortAscending ? ASC : DESC);
        }
        return result;
    }

    private boolean skipValue(Object value) {
        if (value == null) {
            return true;
        }
        if (value instanceof String && StringUtils.isBlank((String) value)) {
            return true;
        }
        if (value instanceof Object[] && ((Object[]) value)[0] == null && ((Object[]) value)[1] == null) {
            return true;
        }
        return false;
    }

    private Object getProperty(Object object, Object property) {
        if (property instanceof Object[]) {
            return getProperties(object, (Object[]) property);
        } else {
            return getProperty(object, (String) property);
        }
    }

    private Object getProperty(Object object, String property) {
        try {
            return PropertyUtils.getProperty(object, property);
        } catch (Exception e) {
            throw new IllegalArgumentException("create condition from object error:" + e.getMessage());
        }
    }

    private Object[] getProperties(Object object, Object[] properties) {
        Object[] values = new Object[properties.length];
        for (int i = 0; i < values.length; i++) {
            values[i] = getProperty(object, (String) properties[i]);
        }
        return values;
    }

    private String getLikeExpression(String property, String alias, String value) {
        parameters.add(value.trim());
        return new StringBuilder(LIKE_OPEN).append(alias).append(DOT).append(property).
                append(LIKE_CLOSE).toString();
    }

    private String getEqExpression(String property, String alias, Object value) {
        return getSimpleExpression(property, alias, EQUALS, value);
    }

    private String getExpression(String property, String alias, Object value) {
        String res;
        if (value instanceof String) {
            res = getLikeExpression(property, alias, (String) value);
        } else if (value instanceof Set) {
            res = getOrExpression(property, alias, (Set) value);
        } else if (value instanceof Object[]) {
            res = getBetweenExpression(property, alias, (Object[]) value);
        } else if (isNullObject(value)) {
            res = getIsNullExpression(property, alias);
        } else {
            res = getEqExpression(property, alias, value);
        }
        return res;
    }

    private boolean isNull(Object object) {
        return NULL_DATE == object || NULL_LONG == object ||
                NULL_SET == object || NULL_STRING == object ||
                NULL_VALUE == object;
    }

    private boolean isNullObject(Object object) {
        if (isNull(object)) {
            return true;
        }
        if (object instanceof Object[]) {
            for (int i = 0; i < ((Object[]) object).length; i++) {
                if (!isNull(((Object[]) object)[i]))
                    return false;
            }
            return true;
        }
        return false;
    }

    private String getIsNullExpression(String property, String alias) {
        return new StringBuilder(alias).append(DOT).append(property).append(IS_NULL).toString();
    }

    private String getBetweenExpression(String property, String alias, Object[] value) {
        if (value.length != 2) {
            throw new IllegalStateException("value.length != 2");
        }
        if (value[0] == null && value[1] != null) {
            return getSimpleExpression(property, alias, EQ_OR_LESS, value[1]);
        } else if (value[1] == null && value[0] != null) {
            return getSimpleExpression(property, alias, MORE_OR_EQ, value[0]);
        } else if (value[0] != null && value[1] != null) {
            parameters.add(value[0]);
            parameters.add(value[1]);
            return new StringBuilder(alias).append(DOT).append(property).append(BETWEEN).toString();
        }
        throw new IllegalArgumentException("can't be null");
    }

    private String getSimpleExpression(String prroperty, String alias, String op, Object value) {
        parameters.add(value);
        return new StringBuilder(alias).append(DOT).append(prroperty).append(op).append('?').toString();
    }

    private String getOrExpression(String property, String alias, Set values) {
        if (values.isEmpty()) {
            return "";
        }
        StringBuilder sb = new StringBuilder(OPEN_BRACKET);
        boolean first = true;
        for (Object value : values) {
            if (!first) {
                sb.append(OR);
            } else {
                first = false;
            }
            sb.append(getExpression(property, alias, value));
        }
        return sb.append(CLOSE_BRACKET).toString();
    }

    public void addParameter(Object value) {
        parameters.add(value);
    }

    public void setParameter(int index, Object value) {
        parameters.set(index, value);
    }

    public QueryBuilder clone() throws CloneNotSupportedException {
        QueryBuilder queryBuilder = new QueryBuilder(alias, orderProperty);
        queryBuilder.first = first;
        queryBuilder.query = new StringBuilder(query);
        queryBuilder.parameters = new ArrayList(parameters);
        return queryBuilder;
    }

}
